﻿/*
 * This class provides the wrapper over the RunTime instance representing the kinect device.
 * We need to manage the stauses and some parementer values fo RunTime instance of kinect device so RunTimeWrapper class is written
 * which saves the states and parameter values for kinect RunTime.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Research.Kinect.Nui;
using System.Drawing;
using System.Windows.Media.Imaging;
using System.Windows;
using System.Drawing.Imaging;
using System.Windows.Media;
using Valentia.Kinection.DataTypes;
using Valentia.Kinection.Configuration;

namespace Valentia.Kinection.Manager
{
    /// <summary>
    /// Run time wrapper class.
    /// </summary>
    public class RuntimeWrapper
    {
        #region CONSTANTS
        /// <summary>
        /// Index of blue byte in 4 byte pixel set of DepthFrame32 array.
        /// </summary>
        internal const int BlueIndex = 0;

        /// <summary>
        /// Index of green byte in 4 byte pixel set of DepthFrame32 array.
        /// </summary>
        internal const int GreenIndex = 1;

        /// <summary>
        /// Index of red byte in 4 byte pixel set of DepthFrame32 array.
        /// </summary>
        internal const int RedIndex = 2;
        #endregion

        #region PROPERTIES
        /// <summary>
        /// Gets the collection of all kinects run time with this machine.
        /// </summary>
        public static IEnumerable<RuntimeWrapper> Kinects
        {
            get
            {
                foreach (Runtime runTime in Runtime.Kinects) { yield return new RuntimeWrapper(runTime); }
            }
        }

        /// <summary>
        /// Gets or sets the run time option which are used to initilize this run time.
        /// </summary>
        public RuntimeOptions RuntimeOptions { get; private set; }

        /// <summary>
        /// Gets the video image resoloution.
        /// </summary>
        public ImageResolution VideoImageResoloution { get; private set; }

        /// <summary>
        /// gets the depth image resoloution
        /// </summary>
        public ImageResolution DepthImageResoloution { get; private set; }

        /// <summary>
        /// Gets the skeleton image resoloution.
        /// </summary>
        public ImageResolution SkeletonImageResoloution { get; private set; }

        /// <summary>
        /// Gets the video image type.
        /// </summary>
        public ImageType VideoImageType { get; private set; }

        /// <summary>
        /// Gets the depth image type.
        /// </summary>
        public ImageType DepthImageType { get; private set; }

        /// <summary>
        /// Gets or sets the current status of kinect run time.
        /// </summary>
        public KinectDeviceStatus? KinectStatus { get; set; }

        /// <summary>
        /// Gets true if no skeleton event is raised for this kinect device runtime else false.
        /// </summary>
        internal bool NoSkeletonReported { get; set; }

        /// <summary>
        /// Gets true if video stream is allowed else false.
        /// </summary>
        public bool AllowedVideoStream { get; private set; }

        /// <summary>
        /// Gets true if depth stream is allowed else false.
        /// </summary>
        public bool AllowedDepthStream { get; private set; }

        /// <summary>
        /// Gets true if skeleton stream is allowed else false.
        /// </summary>
        public bool AllowedSkeletonStream { get; private set; }

        /// <summary>
        /// Gets true if run time is initilized else false.
        /// </summary>
        public bool Initilized { get; private set; }

        /// <summary>
        /// Gets true if this kinect device is active in settings object else false.
        /// </summary>
        public bool Active { get { return this.KinectDeviceEntityObject.Active; } }

        /// <summary>
        /// Gets the key for the kinect run time instance.
        /// </summary>
        public String Key { get; private set; }

        /// <summary>
        /// Gets the depth image frame as 4 bytes for one pixel.
        /// </summary>
        public byte[] DepthFrame32 { get; private set; }

        /// <summary>
        /// Gets or sets the kinect run time instance.
        /// </summary>
        public Runtime KinectRuntime { get; set; }

        /// <summary>
        /// Gets the kinect runtime entitiy object for settings.
        /// </summary>
        public KinectDevice KinectDeviceEntityObject { get; set; }
        #endregion

        #region CONSTRUCTOR
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="kinectRuntime">Kinect run time instance.</param>
        public RuntimeWrapper(Runtime kinectRuntime)
        {
            this.RuntimeOptions = RuntimeOptions.UseDepthAndPlayerIndex | RuntimeOptions.UseSkeletalTracking | RuntimeOptions.UseColor;
            this.VideoImageResoloution = ImageResolution.Resolution640x480;
            this.DepthImageResoloution = ImageResolution.Resolution320x240;
            this.SkeletonImageResoloution = ImageResolution.Resolution640x480;

            this.VideoImageType = ImageType.Color;
            this.DepthImageType = ImageType.DepthAndPlayerIndex;
            this.NoSkeletonReported = false;

            this.KinectStatus = null;

            this.AllowedVideoStream = true;
            this.AllowedDepthStream = true;
            this.AllowedSkeletonStream = true;

            this.Initilized = false;

            this.DepthFrame32 = null;// = new byte[320 * 240 * 4];
            this.KinectDeviceEntityObject = null;
            this.KinectRuntime = kinectRuntime;

            this.Key = this.KinectRuntime.InstanceName;
        }
        #endregion

        #region METHODS
        /// <summary>
        /// Creates and return the key for the provided run time instance.
        /// </summary>
        /// <param name="runtime">Kinect run time instance.</param>
        /// <returns>Created key.</returns>
        public static String GetKey(Runtime runtime) { return runtime.InstanceName; }

        /// <summary>
        /// Populates the settings from kinect device entity object only if run time is not initialized.
        /// </summary>
        /// <param name="kinectDevice">Kinect device entity object.</param>
        public void PopulateSettings(KinectDevice kinectDevice)
        {
            if (this.Initilized) return;

            this.KinectDeviceEntityObject = kinectDevice;
            this.RuntimeOptions = (Microsoft.Research.Kinect.Nui.RuntimeOptions)(kinectDevice.KinectRuntimeOptions);

            this.VideoImageResoloution = (ImageResolution)(kinectDevice.KinectVideoResoloution);
            this.DepthImageResoloution = (ImageResolution)(kinectDevice.KinectDepthResoloution);
            this.SkeletonImageResoloution = (ImageResolution)(kinectDevice.KinectSkeletonResoloution);

            this.AllowedVideoStream = kinectDevice.GenerateVideoStream;
            this.AllowedDepthStream = kinectDevice.GenerateVideoStream;
            this.AllowedSkeletonStream = kinectDevice.GenerateVideoStream;

            this.VideoImageType = (ImageType)(kinectDevice.KinectVideoImageType);
            this.DepthImageType = (ImageType)(kinectDevice.KinectDepthImageType);

            if (!this.AllowedSkeletonStream && this.RuntimeOptions.HasFlag(RuntimeOptions.UseSkeletalTracking))
            {
                this.RuntimeOptions = this.RuntimeOptions & ~(RuntimeOptions.UseSkeletalTracking);
            }
            else if (this.AllowedSkeletonStream && !this.RuntimeOptions.HasFlag(RuntimeOptions.UseSkeletalTracking))
            {
                this.RuntimeOptions = this.RuntimeOptions | RuntimeOptions.UseSkeletalTracking;
            }

            switch (this.DepthImageResoloution)
            {
                case ImageResolution.Resolution80x60: this.DepthFrame32 = new byte[80 * 600 * 4]; break;
                case ImageResolution.Resolution320x240: this.DepthFrame32 = new byte[320 * 240 * 4]; break;
                case ImageResolution.Resolution640x480: this.DepthFrame32 = new byte[640 * 480 * 4]; break;
                case ImageResolution.Resolution1280x1024: this.DepthFrame32 = new byte[1280 * 1024 * 4]; break;
            }
        }

        /// <summary>
        /// Initilizes the kinect run time.
        /// </summary>
        public void Initialize()
        {
            if (this.Initilized) return;

            //To stream color images do following:
            //------------------------------------
            // 1. The options must include UseColor.
            // 2. Valid image resolutions are Resolution1280x1024 and Resolution640x480.
            // 3. Valid image types are Color, ColorYUV, and ColorYUVRaw.

            //To stream depth and player index data do following:
            //---------------------------------------------------
            // 1. The options must include UseDepthAndPlayerIndex.
            // 2. Valid resolutions for depth and player index data are Resolution320x240 and Resolution80x60.
            // 3. The only valid image type is DepthAndPlayerIndex.

            //this.RuntimeOptions = RuntimeOptions.UseDepthAndPlayerIndex | RuntimeOptions.UseSkeletalTracking | RuntimeOptions.UseColor;
            ////runTimeWrapper.RuntimeOptions =
            ////    RuntimeOptions.UseDepthAndPlayerIndex | RuntimeOptions.UseSkeletalTracking | RuntimeOptions.UseColor | RuntimeOptions.UseDepth;
            //this.VideoImageResoloution = ImageResolution.Resolution640x480;
            //this.DepthImageResoloution = ImageResolution.Resolution320x240;

            this.PopulateSettings(this.KinectDeviceEntityObject);
            this.KinectRuntime.Initialize(this.RuntimeOptions);
            this.NoSkeletonReported = false;
            if (this.AllowedVideoStream)
            {
                this.KinectRuntime.VideoStream.Open(ImageStreamType.Video, 2, this.VideoImageResoloution, this.VideoImageType);
            }
            if (this.AllowedDepthStream)
            {
                this.KinectRuntime.DepthStream.Open(ImageStreamType.Depth, 2, this.DepthImageResoloution, this.DepthImageType);
            }

            if (this.AllowedSkeletonStream) //(this.RuntimeOptions.HasFlag(RuntimeOptions.UseSkeletalTracking))
            {
                this.KinectRuntime.SkeletonEngine.TransformSmooth = true;
                this.KinectRuntime.SkeletonEngine.SmoothParameters = new TransformSmoothParameters
                {
                    Smoothing = (float)(this.KinectDeviceEntityObject.SkeletonSmoothing), //1.0f,
                    Correction = (float)(this.KinectDeviceEntityObject.SkeletonCorrection), //0.1f,
                    Prediction = (float)(this.KinectDeviceEntityObject.SkeletonPrediction), //0.1f,
                    JitterRadius = (float)(this.KinectDeviceEntityObject.SkeletonJitterRadius), //0.05f,
                    MaxDeviationRadius = (float)(this.KinectDeviceEntityObject.SkeletonMaxDeviationRadius) //0.05f
                };
            }
            this.Initilized = true;
        }

        /// <summary>
        /// Uninitialize the kinect run time.
        /// </summary>
        internal void Uninitialize()
        {
            if (!this.Initilized) return;

            this.KinectRuntime.Uninitialize();
            this.NoSkeletonReported = false;
            this.Initilized = false;
        }

        /// <summary>
        /// Gets the bitmap image from image frame instance.
        /// </summary>
        /// <param name="imageFrame">Imge frame instance.</param>
        /// <param name="isDepthImage">True for the depth image else false for video image.</param>
        /// <returns>Bitmap image</returns>
        public Bitmap GetBitmap(ImageFrame imageFrame, bool isDepthImage)
        {
            return this.GetBitmap(imageFrame.Image, isDepthImage);
        }

        /// <summary>
        /// Gets the bitmap image from planar image instance.
        /// </summary>
        /// <param name="planarImage">Planar image instance.</param>
        /// <param name="isDepthImage">True for the depth image else false for video image.</param>
        /// <returns>Bitmap image</returns>
        public Bitmap GetBitmap(PlanarImage planarImage, bool isDepthImage)
        {
            byte[] bits = planarImage.Bits;
            int bytesPerPixesl = planarImage.BytesPerPixel;
            if (isDepthImage)
            {
                this.ConvertDepthFrame(bits);
                bits = this.DepthFrame32;
                bytesPerPixesl = 4;
            }

            BitmapSource source =
                BitmapSource.Create(planarImage.Width, planarImage.Height, 96, 96, PixelFormats.Bgr32, null, bits, planarImage.Width * bytesPerPixesl);

            Bitmap bmp = new Bitmap(source.PixelWidth, source.PixelHeight, System.Drawing.Imaging.PixelFormat.Format32bppRgb);
            BitmapData data =
                bmp.LockBits(new Rectangle(System.Drawing.Point.Empty, bmp.Size), ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppRgb);

            source.CopyPixels(new Int32Rect(0, 0, bmp.Width, bmp.Height), data.Scan0, data.Height * data.Stride, data.Stride);
            bmp.UnlockBits(data);
            return bmp;
        }

        /// <summary>
        /// Converts the 16 bits per pixel image frame to 32 bit per pixel image frame for the depth image.
        /// </summary>
        /// <param name="depthFrame16">16 bits per pixel image generated form kinect instance.</param>
        public void ConvertDepthFrame(byte[] depthFrame16)
        {
            //This method's code is described as follow
            //-----------------------------------------
            //depthFrame16 buffer has following format.
            // -    The low-order 3 bits contain a skeleton ID.
            // -    The remaining bits contain the depth value in millimeters.
            //If the application displayed the depth frame as raw, gray-scale data, users would find it difficult to distinguish the individuals in the scene.
            //Therefore, the sample converts the 16-bit gray-scale data into 32-bit RGB data, with each person in a different color. The conversion routine
            //operates on a byte array of depth data and steps through the array 2 bytes at a time. It follows these steps:
            // 1.   Store the player number from the low-order 3 bits of the first byte in the player variable. Values range from 0 to 6. A value equal to 0
            //      indicates that no player is present.
            // 2.   Retrieve the 11 bits of actual depth data from the low-order 3 bits of the second byte and the high-order 5 bits of the first byte and store
            //      it in the realDepth variable.
            // 3.   Invert the result to generate an 8-bit intensity value that is appropriate for display, so that objects that are closer to the camera appear
            //      brighter and objects that are farther away appear darker. It ignores the most significant bit in this calculation.
            // 4.   Zero-initialize the corresponding elements of the 32-bit frame.
            // 5.   Assign red, green, and blue color values to the result based on the player number, by using the RedIndex, GreenIndex, and BlueIndex constants to
            //      index into the array.

            bool hasPlayerData = this.RuntimeOptions.HasFlag(RuntimeOptions.UseDepthAndPlayerIndex);
            for (int i16 = 0, i32 = 0; i16 < depthFrame16.Length && i32 < this.DepthFrame32.Length; i16 += 2, i32 += 4)
            {
                int player = hasPlayerData ? depthFrame16[i16] & 0x07 : -1;
                int realDepth = 0;

                if (hasPlayerData)
                {
                    realDepth = (depthFrame16[i16 + 1] << 5) | (depthFrame16[i16] >> 3);
                }
                else
                {
                    realDepth = (depthFrame16[i16 + 1] << 8) | (depthFrame16[i16]);
                }

                // transform 13-bit depth information into an 8-bit intensity appropriate
                // for display (we disregard information in most significant bit)
                byte intensity = (byte)(255 - (255 * realDepth / 0x0fff));

                this.DepthFrame32[i32 + RuntimeWrapper.RedIndex] = 0;
                this.DepthFrame32[i32 + RuntimeWrapper.GreenIndex] = 0;
                this.DepthFrame32[i32 + RuntimeWrapper.BlueIndex] = 0;

                // choose different display colors based on player
                switch (player)
                {
                    case -1:
                    case 0:
                        this.DepthFrame32[i32 + RuntimeWrapper.RedIndex] = (byte)(intensity / 2);
                        this.DepthFrame32[i32 + RuntimeWrapper.GreenIndex] = (byte)(intensity / 2);
                        this.DepthFrame32[i32 + RuntimeWrapper.BlueIndex] = (byte)(intensity / 2);
                        break;
                    case 1:
                        this.DepthFrame32[i32 + RuntimeWrapper.RedIndex] = intensity;
                        break;
                    case 2:
                        this.DepthFrame32[i32 + RuntimeWrapper.GreenIndex] = intensity;
                        break;
                    case 3:
                        this.DepthFrame32[i32 + RuntimeWrapper.RedIndex] = (byte)(intensity / 4);
                        this.DepthFrame32[i32 + RuntimeWrapper.GreenIndex] = (byte)(intensity);
                        this.DepthFrame32[i32 + RuntimeWrapper.BlueIndex] = (byte)(intensity);
                        break;
                    case 4:
                        this.DepthFrame32[i32 + RuntimeWrapper.RedIndex] = (byte)(intensity);
                        this.DepthFrame32[i32 + RuntimeWrapper.GreenIndex] = (byte)(intensity);
                        this.DepthFrame32[i32 + RuntimeWrapper.BlueIndex] = (byte)(intensity / 4);
                        break;
                    case 5:
                        this.DepthFrame32[i32 + RuntimeWrapper.RedIndex] = (byte)(intensity);
                        this.DepthFrame32[i32 + RuntimeWrapper.GreenIndex] = (byte)(intensity / 4);
                        this.DepthFrame32[i32 + RuntimeWrapper.BlueIndex] = (byte)(intensity);
                        break;
                    case 6:
                        this.DepthFrame32[i32 + RuntimeWrapper.RedIndex] = (byte)(intensity / 2);
                        this.DepthFrame32[i32 + RuntimeWrapper.GreenIndex] = (byte)(intensity / 2);
                        this.DepthFrame32[i32 + RuntimeWrapper.BlueIndex] = (byte)(intensity);
                        break;
                    case 7:
                        this.DepthFrame32[i32 + RuntimeWrapper.RedIndex] = (byte)(255 - intensity);
                        this.DepthFrame32[i32 + RuntimeWrapper.GreenIndex] = (byte)(255 - intensity);
                        this.DepthFrame32[i32 + RuntimeWrapper.BlueIndex] = (byte)(255 - intensity);
                        break;
                }
            }
        }

        /// <summary>
        /// Generates the collection of skeleton points from skeleton data.
        /// </summary>
        /// <param name="skeletonData">Skeleton data,</param>
        /// <returns>Collection of skeleton points.</returns>
        public SkeletonJointCollection GetSkeletonPoints(SkeletonData skeletonData)
        {
            //SkeletonJointCollection points = new SkeletonJointCollection(skeletonData.TrackingID, skeletonData.UserIndex, skeletonData.Joints.Count);
            Dictionary<SkeletonJointID, SkeletonJointInfo> mappedJoints = new Dictionary<SkeletonJointID, SkeletonJointInfo>(skeletonData.Joints.Count);
            SkeletonJointInfo aJoint = null;
            foreach (Joint joint in skeletonData.Joints)
            {
                aJoint = this.GetJointPoint(joint);
                mappedJoints.Add(aJoint.JointID, aJoint);
            }

            return new SkeletonJointCollection(skeletonData.TrackingID, skeletonData.UserIndex, ((KinectSkeletonQuality)(skeletonData.Quality)), mappedJoints);
        }

        /// <summary>
        /// Generates the skeleton joint instance from joint.
        /// </summary>
        /// <param name="joint">Joint.</param>
        /// <returns>Skeleton joint instance.</returns>
        public SkeletonJointInfo GetJointPoint(Joint joint)
        {
            SkeletonJointVector vecor = new SkeletonJointVector(joint.Position.W, joint.Position.X, joint.Position.Y, joint.Position.Z);

            float depthX, depthY; short depthValue;
            this.KinectRuntime.SkeletonEngine.SkeletonToDepthImage(joint.Position, out depthX, out depthY, out depthValue);
            SkeletonJointDepth jointDepth = new SkeletonJointDepth(depthX, depthY, depthValue);

            switch (this.DepthImageResoloution)
            {
                case ImageResolution.Resolution1280x1024:
                    {
                        depthX = depthX * 1280; //convert to 1280, 1024 space
                        depthY = depthY * 1024; //convert to 1280, 1024 space
                    } break;
                case ImageResolution.Resolution640x480:
                    {
                        depthX = depthX * 640; //convert to 640, 480 space
                        depthY = depthY * 480; //convert to 640, 480 space
                    } break;
                case ImageResolution.Resolution320x240:
                    {
                        depthX = depthX * 320; //convert to 320, 240 space
                        depthY = depthY * 240; //convert to 320, 240 space
                    } break;
                default: // ImageResolution.Resolution80x60:
                    {
                        depthX = depthX * 80; //convert to 80, 60 space
                        depthY = depthY * 60; //convert to 80, 60 space
                    } break;
            }

            int colorX, colorY;
            ImageViewArea iv = new ImageViewArea();

            // only ImageResolution.Resolution640x480 is supported at this point
            this.KinectRuntime.NuiCamera.GetColorPixelCoordinatesFromDepthPixel(
                this.VideoImageResoloution, iv, (int)depthX, (int)depthY, depthValue, out colorX, out colorY);

            //double canvasWidth = 640, canvasHeight = 480;
            //// map back to skeleton.Width & skeleton.Height
            //return new SkeletonJoint(joint.ID, (canvasWidth * colorX / 640.0), (canvasHeight * colorY / 480));

            return new SkeletonJointInfo((SkeletonJointID)(joint.ID), colorX, colorY, vecor, jointDepth);
        }
        #endregion
    }
}